﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;

namespace ServerApp
{
    static partial class Server
    {
        static int QueueLength = 10;    //размер очереди входящих сообщений
        static int BufferSize = 256;    //размер буфера для входящих сообщений

        static Socket ServerSocket;                         //сокет сервера
        static List<Player> Players = new List<Player>();   //коллекция клиентов, подключенных к серверу
        
        static List<Player> PlayersReady = new List<Player>(); //авторизованные клиенты

        /// <summary>
        /// Команды для пересылки
        /// </summary>
        class Command
        {
            string type;
            string[] args;

            public string Type { get { return type; } }
            public string[] Args { get { return args; } }

            public Command(string t, params string[] p)
            {
                type = t;
                args = p;
            }
        }

        /// <summary>
        /// Игрок(клиент)
        /// </summary>
        class Player
        {
            public Socket socket;                   //сокет клиента
            public string nickname = string.Empty;  //ник игрока
            public Thread thread;                   //поток обработки сообщений
            public bool isAuthorized = false;       //авторизован/нет

            /// <summary>
            /// Получение сообщения от клиента
            /// </summary>
            /// <param name="com"></param>
            /// <returns></returns>
            public bool Receive(out Command com)
            {
                com = null;
                byte[] buf = new byte[BufferSize];

                //принимаем
                try
                {
                    socket.Receive(buf);
                }
                catch (Exception e)
                {
                    //невозможно получить сбщ
                    lock (Players) Players.Remove(this);
                    Logger.WriteLog(string.Format("Connection interrupted. IP: {0}. Total connections: {2}. Error: '{1}'", socket.RemoteEndPoint.ToString(), e.Message, Players.Count));
                    return false;
                }

                //обрабатываем
                string s = Encoding.ASCII.GetString(buf).Trim();
                if (s.IndexOf(@"\EOM") == -1)
                {
                    Send(new Command("error: wrong command format"));
                    Logger.WriteLog(string.Format("Received. From: {0}; Error: wrong formed command", socket.RemoteEndPoint.ToString()));
                    return false;
                }
                else
                {
                    s = s.Substring(0, s.IndexOf(@"\EOM"));
                    Logger.WriteLog(string.Format("Received. From: {0}; Msg: '{1}'", socket.RemoteEndPoint.ToString(), s));

                    string[] mas = s.Split(new char[] { ' ', '=', '(', ',', ')' });
                    string t = mas[0];
                    string[] args = new string[mas.Length - 1];
                    for (int i = 1; i < mas.Length; i++)
                        args[i - 1] = mas[i];
                    com = new Command(t, args);
                    return true;
                }
            }

            /// <summary>
            /// Отсылка сообщения клиенту
            /// </summary>
            /// <param name="com">Команда для пересылки</param>
            /// <returns>Успех/провал операции</returns>
            public bool Send(Command com)
            {
                string s = com.Type + " ";
                for (int i = 0; i < com.Args.Length; i++)
                    s += com.Args[i];
                
                //конвертация клиенту
                if (com.Type == "sync")
                {
                    int[] xy = ConvertToClient(int.Parse(com.Args[1]), int.Parse(com.Args[2]));
                    Logger.WriteLog(string.Format("Convertation ({0},{1}) -> ({2},{3})", com.Args[1], com.Args[2], xy[0], xy[1]));
                    s = string.Format("{0} {1}=({2},{3})", com.Type, com.Args[0], xy[0].ToString(), xy[1].ToString());
                }

                s += @"\EOM";

                try
                {
                    socket.Send(Encoding.ASCII.GetBytes(s));
                    Logger.WriteLog(string.Format("Sent. To: {0}; Msg: '{1}'", socket.RemoteEndPoint.ToString(), s));
                }
                catch (Exception e)
                {
                    Logger.WriteLog(string.Format("Error sending message to {0}: {1}", socket.RemoteEndPoint.ToString(), e.Message));
                }
                return true;
            }
        }

        /// <summary>
        /// Запуск и настройка сервера
        /// </summary>
        public static void Start(IPAddress ip, int port)
        {
            ClientFieldInit();
            Logger.WriteLog(string.Format("Starting server on {0}:{1}", ip.ToString(), port));
            IPEndPoint serverEndPoint = new IPEndPoint(ip, port);
            ServerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            ServerSocket.Bind(serverEndPoint);
            ServerSocket.Listen(QueueLength);
            Thread ac = new Thread(AcceptConnections);
            ac.IsBackground = true;
            ac.Start();
        }

        /// <summary>
        /// Принимает входящие соединения
        /// </summary>
        private static void AcceptConnections()
        {
            Logger.WriteLog("Accepting thread started");
            while (true)
            {
                Socket socket = ServerSocket.Accept();
                if (socket != null)
                {
                    Player player = new Player();
                    player.socket = socket;

                    player.thread = new Thread(ProcessConnection);
                    player.thread.IsBackground = true;
                    player.thread.Start(player);

                    lock (Players) Players.Add(player);
                    Logger.WriteLog(string.Format("Connection accepted. IP: {0}. Total connections: {1}", player.socket.RemoteEndPoint.ToString(), Players.Count));
                }
            }
        }

        /// <summary>
        /// Обработка сообщений клиента
        /// </summary>
        private static void ProcessConnection(object arg)
        {
            Player player = (Player)arg;
            Command com;

            while (true)
            {
                if (player.Receive(out com))
                {
                    if (CheckCommand(ref com))
                    {
                        switch (com.Type)
                        {
                            case "auth":

                                //авторизация
                                player.isAuthorized = Authorize(com.Args[0], com.Args[1], player);
                                if (player.isAuthorized)
                                    player.Send(new Command("success"));
                                else
                                    player.Send(new Command("denied"));

                                break;
                            case "register":

                                //регистрация с авторизацией
                                player.isAuthorized = Register(com.Args[0], com.Args[1], player);
                                if (player.isAuthorized)
                                    player.Send(new Command("success"));
                                else
                                    player.Send(new Command("denied"));

                                break;
                            case "ready":

                                //готовность к началу игры
                                if (player.isAuthorized)
                                {
                                    player.Send(new Command("searching for an opponent for you..."));
                                    lock (PlayersReady)
                                    {
                                        PlayersReady.Add(player);
                                        Logger.WriteLog("players ready: " + PlayersReady.Count);
                                        if (PlayersReady.Count > 1)
                                        {
                                            //старт игры. запуск потока игры
                                            Thread Game = new Thread(ProcessGame);
                                            Game.IsBackground = true;
                                            Game.Start();
                                        }
                                    }
                                    Thread.CurrentThread.Suspend();
                                }
                                else
                                    player.Send(new Command("not authorized"));
                                
                                break;
                            case "stats":

                                //получить статистику
                                player.Send(new Command("u requested stats"));
                                if (player.isAuthorized)
                                {
                                    //высылаем статистику
                                }

                                break;
                            default:
                                
                                player.Send(new Command("unknown command"));

                                break;
                        }
                    }
                    else
                        player.Send(new Command("wrong command format"));
                }
                else
                    break;
            }
        }

    }

    static partial class Server
    {
        /// <summary>
        /// Попытка авторизации пользователя
        /// </summary>
        /// <param name="login">логин</param>
        /// <param name="pass">пароль</param>
        /// <param name="player">игрок, выполняющий попытку авторизации</param>
        /// <returns>успех/провал операции</returns>
        static private bool Authorize(string login, string pass, Player player)
        {
            lock (Players)
            for (int i = 0; i < Players.Count;i++ )
                if((Players[i].isAuthorized)&&(Players[i].nickname==login))
                    return false;
            
            Logger.WriteLog(string.Format("Trying to authorize user '{0}'-'{1}'", login, pass));
            string command = string.Format("Select count(*) From dbo.Accounts Where Login='{0}' And Pass='{1}'", login, pass);
            SqlConnection con = new SqlConnection(ConfigurationManager.ConnectionStrings["MillConnection"].ConnectionString);
            SqlCommand com = new SqlCommand(command, con);
            int count = 0;
            try
            {
                con.Open();
                count = (int)com.ExecuteScalar();
            }
            catch (Exception e)
            {
                Logger.WriteLog("Error authorizing user: " + e.Message);
                return false;
            }
            finally
            {
                con.Close();
            }
            if (count == 0)
                return false;
            if (count > 0)
            {
                player.nickname = login;
                return true;
            }
            return false;
        }

        /// <summary>
        /// Попытка регистрации пользователя
        /// </summary>
        /// <param name="login">логин</param>
        /// <param name="pass">пароль</param>
        /// <param name="player">игрок, выполняющий попытку регистрации</param>
        /// <returns>успех/провал операции</returns>
        static private bool Register(string login, string pass, Player player)
        {
            Logger.WriteLog(string.Format("Trying to register user '{0}'-'{1}'", login, pass));
            string command = string.Format("select count(*) from dbo.Accounts where Login='{0}'", login);
            SqlConnection con = new SqlConnection(ConfigurationManager.ConnectionStrings["MillConnection"].ConnectionString);
            SqlCommand com = new SqlCommand(command, con);
            try
            {
                con.Open();
                int count = (int)com.ExecuteScalar();
                if (count > 0)
                    return false;
                if (count == 0)
                {
                    command = string.Format("insert into dbo.Accounts values(newid(), '{0}', '{1}', getdate())", login, pass);
                    com.CommandText = command;
                    com.ExecuteNonQuery();
                    return true;
                }
            }
            catch (Exception e)
            {
                Logger.WriteLog("Error registering user: " + e.Message);
                return false;
            }
            finally
            {
                con.Close();
            }
            return false;
        }

        /// <summary>
        /// Ведёт игру между двумя игроками
        /// </summary>
        /// <param name="o1">Игрок1</param>
        /// <param name="o2">Игрок2</param>
        static private void ProcessGame()
        {
            //приостановка потоков-обработчиков для игроков, которые начали игру

            Player leader;
            Player opponent;

            //выбор очерёдности хода
            Random r = new Random();
            int n = r.Next(0, 100) % 2;
            
            lock (PlayersReady)
            {
                if (n == 0)
                {
                    leader = PlayersReady[0];
                    opponent = PlayersReady[1];
                }
                else
                {
                    leader = PlayersReady[1];
                    opponent = PlayersReady[0];
                }
                PlayersReady.Remove(leader);
                PlayersReady.Remove(opponent);
            }

            Logger.WriteLog(string.Format("Game started for {0}, {1}", leader.nickname, opponent.nickname));

            leader.Send(new Command("lead=true " + opponent.nickname));
            opponent.Send(new Command("lead=false " + leader.nickname));
            
            Field field = new Field();

            int wins = 0;                                   //количество собранных мельниц
            int StepsCounter = 0;                           //счётчик ходов
            Command curCommand;                             //текущая команда
            Command nextCommand = new Command("place");     //следующая команда
            bool isPlaced = false;                          //флаг завершения хода
            bool isRemoved = false;

            //расстановка фишек
            bool isEndOfFirstRound = false;
            while (!isEndOfFirstRound)
            {
                //получение сообщения
                if (leader.Receive(out curCommand))
                {
                    Logger.WriteLog(string.Format("{0}'s message received", leader.nickname));
                    //проверка аргументов
                    if (CheckCommand(ref curCommand))
                    {
                        Logger.WriteLog(string.Format("{0}'s message checked", leader.nickname));
                        //проверка соответствия ожидаемой
                        if (curCommand.Type != nextCommand.Type)
                            leader.Send(new Command("wrong action"));
                        else
                        {
                            switch (curCommand.Type)
                            {
                                case "place":

                                    Field prevField = new Field(field);
                                    isPlaced = field.TryPlace(int.Parse(curCommand.Args[1]), int.Parse(curCommand.Args[2]), leader.nickname);
                                    if (isPlaced)
                                        wins = field.Compare(prevField, leader.nickname);
                                    break;

                                case "remove":

                                    isRemoved = field.TryRemove(int.Parse(curCommand.Args[1]), int.Parse(curCommand.Args[2]), leader.nickname);
                                    if (isRemoved)
                                        wins--;
                                    break;
                            }

                            //ответ
                            if ((isPlaced) || (isRemoved))
                            {
                                leader.Send(new Command(string.Format("wins={0}", wins)));
                                opponent.Send(new Command("sync", curCommand.Type, curCommand.Args[1], curCommand.Args[2]));
                            }
                            else
                                leader.Send(new Command("denied"));

                            //обработка результатов
                            if (wins > 0)
                                nextCommand = new Command("remove");
                            if ((wins == 0) && (isPlaced))
                            {
                                nextCommand = new Command("place"); //следующей ждём place

                                leader.Send(new Command("lead=false"));
                                opponent.Send(new Command("lead=true"));

                                Player p = leader;
                                leader = opponent;
                                opponent = p;

                                StepsCounter++;

                                isPlaced = false;
                                isRemoved = false;
                            }

                            //проверка завершения этапа
                            if (StepsCounter / 2 == 9) //по 9 фишек на игрока
                                isEndOfFirstRound = true;
                        }
                    }
                    else
                        leader.Send(new Command("wrong command"));
                }
                else
                {
                    opponent.Send(new Command("opponent left the game"));
                    break;
                }
            }
            
            bool isEndOfGame = false;   //флаг конца игры
            bool isTaken = false;       //попытка взять фишку
            bool isMoved = false;       //попытка переместить фишку
            int[] Taken = new int[2];   //координаты взятой фишки
            wins = 0;
            
            if(isEndOfFirstRound)
            {
                while (!isEndOfGame)
                {
                    //получение сообщения
                    if (leader.Receive(out curCommand))
                    {
                        Logger.WriteLog(string.Format("{0}'s message received", leader.nickname));
                        //проверка аргументов
                        if (CheckCommand(ref curCommand))
                        {
                            Logger.WriteLog(string.Format("{0}'s message checked", leader.nickname));
                            switch (curCommand.Type)
                            {
                                case "take":

                                    if (wins == 0)
                                    {
                                        isTaken = field.TryTake(int.Parse(curCommand.Args[1]), int.Parse(curCommand.Args[2]), leader.nickname);
                                        if (isTaken)
                                        {
                                            Taken[0] = int.Parse(curCommand.Args[1]);
                                            Taken[1] = int.Parse(curCommand.Args[2]);
                                            leader.Send(new Command("success"));
                                            opponent.Send(new Command("sync", curCommand.Type, curCommand.Args[1], curCommand.Args[2]));
                                        }
                                        else
                                            leader.Send(new Command("denied"));
                                    }

                                    break;
                                case "move":

                                    if (isTaken && (wins == 0))
                                    {
                                        Field prevField = new Field(field);
                                        isMoved = field.TryMove(Taken[0], Taken[1], int.Parse(curCommand.Args[1]), int.Parse(curCommand.Args[2]), leader.nickname);
                                        if (isMoved)
                                        {
                                            wins = field.Compare(prevField, leader.nickname);
                                            leader.Send(new Command(string.Format("wins={0}", wins)));
                                            opponent.Send(new Command("sync", curCommand.Type, curCommand.Args[1], curCommand.Args[2]));
                                        }
                                        else
                                            leader.Send(new Command("denied"));
                                    }
                                    break;

                                case "remove":

                                    if (wins != 0)
                                    {
                                        isRemoved = field.TryRemove(int.Parse(curCommand.Args[1]), int.Parse(curCommand.Args[2]), leader.nickname);
                                        if (isRemoved)
                                        {
                                            wins--;
                                            leader.Send(new Command(string.Format("wins={0}", wins)));
                                            opponent.Send(new Command("sync", curCommand.Type, curCommand.Args[1], curCommand.Args[2]));
                                        }
                                        else
                                            leader.Send(new Command("denied"));
                                    }
                                    break;

                                default:
                                    leader.Send(new Command("wrong action"));
                                    break;
                            }

                            if (isMoved && (wins == 0))
                            {
                                leader.Send(new Command("lead=false"));
                                opponent.Send(new Command("lead=true"));

                                Player p = leader;
                                leader = opponent;
                                opponent = p;

                                isTaken = false;
                                isMoved = false;
                                isRemoved = false;
                            }
                            //проверка конца игры
                            isEndOfGame = field.isEnded(leader.nickname, opponent.nickname);
                        }
                        else
                            leader.Send(new Command("wrong command"));
                    }
                    else
                    {
                        opponent.Send(new Command("opponent left the game"));
                        break;
                    }
                }
                //обработка и отсылка результатов игры
                leader.Send(new Command("game over"));
                opponent.Send(new Command("game over"));
            }
            //возобновление потоков-обработчиков
            leader.thread.Resume();
            opponent.thread.Resume();
        }

        /// <summary>
        /// Проверяет аргументы команды
        /// </summary>
        /// <param name="com"></param>
        /// <returns></returns>
        static private bool CheckCommand(ref Command com)
        {
            if ((com.Type == "register") || (com.Type == "auth"))
            {
                if (com.Args.Length != 2)
                    return false;
            }
            if ((com.Type == "place") || (com.Type == "take") || (com.Type == "move") || (com.Type == "remove"))
            {
                //проверка количества аргументов
                if (com.Args.Length != 4)
                    return false;
                //проверка типа аргументов
                int x = 0;
                int y = 0;
                try
                {
                    x = int.Parse(com.Args[1]);
                    y = int.Parse(com.Args[2]);
                }
                catch
                {
                    return false;
                }
                //конвертация координат и проверка существования точки на поле
                int[] rp = ConvertToServer(x, y);
                Logger.WriteLog(string.Format("Convertation ({0},{1}) -> ({2},{3})", x, y, rp[0], rp[1]));
                com = new Command(com.Type, com.Args[0], rp[0].ToString(), rp[1].ToString(), com.Args[1]);
                Field f = new Field();
                if (f.IndexOf(rp[0], rp[1]) == -1)
                    return false;
            }
            return true;
        }

        /// <summary>
        /// Смена координат между клиентом и сервером
        /// </summary>
        #region CoordinatesConvertation
        struct ClientSpot
        {
            public int x;
            public int y;
        }
        
        private static ClientSpot[] ClientField;
        
        private static void ClientFieldInit()
        {
            ClientField = new ClientSpot[24];
            //заполнение клиентского поля
            int Counter = 0;
            for (int i = 1; i < 8; i++)
            {
                if (i < 4)
                {
                    ClientField[Counter].x = i;
                    ClientField[Counter].y = i;
                    Counter++;
                    ClientField[Counter].x = i;
                    ClientField[Counter].y = 4;
                    Counter++;
                    ClientField[Counter].x = i;
                    ClientField[Counter].y = 8 - i;
                    Counter++;
                }
                if (i == 4)
                {
                    for (int j = 1; j < 8; j++)
                        if (j != 4)
                        {
                            ClientField[Counter].x = i;
                            ClientField[Counter].y = j;
                            Counter++;
                        }
                }
                if (i > 4)
                {
                    ClientField[Counter].x = i;
                    ClientField[Counter].y = 8 - i;
                    Counter++;
                    ClientField[Counter].x = i;
                    ClientField[Counter].y = 4;
                    Counter++;
                    ClientField[Counter].x = i;
                    ClientField[Counter].y = i;
                    Counter++;
                }
            }
        }
        public static int[] ConvertToServer(int x, int y)
        {
            Field f = new Field();
            int[] res = new int[2];
            int index = 0;
            for (int i = 0; i < ClientField.Length; i++)
                if ((ClientField[i].x == x) && (ClientField[i].y == y))
                {
                    index = i;
                    break;
                }

            if (index < 9)
            {
                res[0] = x;
                if ((index % 3 == 0) || (index % 3 == 1))
                    res[1] = y;
                if (index % 3 == 2)
                    res[1] = y + 1;
            }
            if ((index >= 9) && (index < 12))
            {
                res[0] = x;
                res[1] = y;
            }
            if ((index >= 12) && (index < 15))
            {
                res[0] = x + 1;
                res[1] = y + 1;
            }
            if ((index >= 15) && (index < 24))
            {
                res[0] = x + 1;
                if (index % 3 == 0)
                    res[1] = y;
                if ((index % 3 == 1) || (index % 3 == 2))
                    res[1] = y + 1;
            }

            return res;
        }
        public static int[] ConvertToClient(int r, int p)
        {
            int[] res = new int[2];
            Field f = new Field();
            int index = f.IndexOf(r, p);

            if (index < 9)
            {
                res[0] = r;
                if ((index % 3 == 0) || (index % 3 == 1))
                    res[1] = p;
                if (index % 3 == 2)
                    res[1] = p - 1;
            }
            if ((index >= 9) && (index < 12))
            {
                res[0] = r;
                res[1] = p;
            }
            if ((index >= 12) && (index < 15))
            {
                res[0] = r - 1;
                res[1] = p - 1;
            }
            if ((index >= 15) && (index < 24))
            {
                res[0] = r - 1;
                if (index % 3 == 0)
                    res[1] = p;
                if ((index % 3 == 1) || (index % 3 == 2))
                    res[1] = p - 1;
            }

            return res;
        }
        #endregion
    }

    /// <summary>
    /// Пишет лог
    /// </summary>
    static class Logger
    {
        /// <summary>
        /// Пишет лог в каталог с exe-файлом
        /// </summary>
        /// <param name="msg"></param>
        public static void WriteLog(string msg)
        {
            string[] s = new string[1];
            s[0] = string.Format("{0}: {1}", DateTime.Now.ToString(), msg);
            try
            {
                System.IO.File.AppendAllLines(System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Log.txt"), s);
            }
            catch
            {}
        }
    }
}
